/**
 *    Copyright (C) 2007-2009 DIVIS GmbH (http://www.divis-hagen.de/)
 * 
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 3 of the License, or (at your option) any later version.
 * 
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 * 
 *    You should have received a copy of the GNU Lesser General
 *    Public License along with this library; if not, write to the
 *    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *    Boston, MA 02110-1301 USA
 */
package de.divis_hagen.json;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;


/**
 *
 * @author Aike J Sommer
 */
public class TreeBuilder {

    private JSONContextImpl context;

    private int depth;

    public TreeBuilder(JSONContextImpl context) {
        this.context = context;
    }

    public JSONTree build(Object o) throws JSONException {
        JSONTree tree = new JSONTree();
        depth = 0;
        tree.setRoot(buildSub(o == null ? null : o.getClass(), o, false));

        return tree;
    }

    public JSONTree build(Class type) throws JSONException {
        JSONTree tree = new JSONTree();
        depth = 0;
        tree.setRoot(buildSub(type, null, true));

        return tree;
    }

    private JSONObjectBase buildSub(Class type, Object o, boolean typeTree) throws JSONException {
        if (type == null || (! typeTree && o == null)) {
            return buildPrimitive(null);
        } else if (type.isArray()) {
            return buildArray(type.getComponentType(), (Object[]) o, typeTree);
        } else if (o instanceof Collection) {
            return buildCollection((Collection) o, typeTree);
        } else if (o instanceof Map) {
            return buildMap((Map) o, typeTree);
        } else if (depth == 0 && type.isAnnotationPresent(XmlRootElement.class)) {
            return buildRootObject(type, o, typeTree);
        } else if (type.isAnnotationPresent(XmlType.class) || type.
                isAnnotationPresent(XmlRootElement.class)) {
            return buildObject(type, o, typeTree);
        }

        return buildPrimitive(o);
    }

    private JSONArray buildArray(Class elementType, Object[] array, boolean typeTree) throws JSONException {
        depth++;

        JSONArray result = new JSONArray();
        if (array != null) {
            for (int i = 0; i < array.length; i++) {
                Object object = array[i];
                result.add(buildSub(object == null ? null : object.getClass(), object, typeTree));
            }
        }

        depth--;
        return result;
    }

    private JSONArray buildCollection(Collection data, boolean typeTree) throws JSONException {
        depth++;

        JSONArray result = new JSONArray();
        if (data != null) {
            for (Object object : data) {
                result.add(buildSub(object == null ? null : object.getClass(), object, typeTree));
            }
        }

        depth--;
        return result;
    }

    private JSONObject buildMap(Map<Object, Object> data, boolean typeTree) throws JSONException {
        depth++;

        JSONObject result = new JSONObject();
        if (data != null) {
            for (Map.Entry entry : data.entrySet()) {
                result.put(context.toIdentifier(entry.getKey().toString()),
                        buildSub(entry.getValue() == null ? null : entry.getValue().getClass(),
                        entry.getValue(), typeTree));
            }
        }

        depth--;
        return result;
    }

    private JSONObject buildObject(Class type, Object data, boolean typeTree) throws JSONException {
        depth++;

        JSONObject result = new JSONObject();
        Map<String, Object> values = new HashMap();
        Map<String, Class> types = new HashMap();

        Method[] methods = type.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if (!method.isAnnotationPresent(XmlTransient.class) && method.getName().
                    startsWith("get") && method.getParameterTypes().length == 0 && !method.getReturnType().
                    equals(Void.TYPE) && !"getClass".equals(method.getName())) {
                String name = null;
                if (method.isAnnotationPresent(JSONElement.class)) {
                    name = method.getAnnotation(JSONElement.class).name();
                    if (!name.equals("##default")) {
                        name = context.toIdentifier(name);
                    } else {
                        name = null;
                    }
                }
                if (method.isAnnotationPresent(XmlElement.class)) {
                    if (name == null) {
                        name = method.getAnnotation(XmlElement.class).name();
                        if (!name.equals("##default")) {
                            name = context.toIdentifier(name);
                        } else {
                            name = null;
                        }
                    }
                }
                if (name == null) {
                    name = method.getName().substring(3);
                    name = name.substring(0, 1).toLowerCase() + name.substring(1);
                    name = context.toIdentifier(name);
                }
//                XmlElement element = method.getAnnotation(XmlElement.class);
//                String name;
//                if (element != null && !element.name().equals("##default")) {
//                    name = context.toIdentifier(element.name());
//                } else {
//                    name = method.getName().substring(3);
//                    name = name.substring(0, 1).toLowerCase() +
//                            name.substring(1);
//                    name = context.toIdentifier(name);
//                }
                if (!name.equals("class") && !values.containsKey(name)) {
                    try {
                        Object value = data == null ? null : method.invoke(data);
                        values.put(name, value);
                        types.put(name, value == null ? method.getReturnType() : value.getClass());
                    } catch (Exception ex) {
                        throw new JSONException(ex);
                    }
                }
            }
        }

        Class c = type;
        while (c != null) {
            Field[] fields = c.getDeclaredFields();
            for (Field field : fields) {
                if (!field.isAnnotationPresent(XmlTransient.class)) {
                    String name = null;
                    if (field.isAnnotationPresent(JSONElement.class)) {
                        name = field.getAnnotation(JSONElement.class).name();
                        if (!name.equals("##default")) {
                            name = context.toIdentifier(name);
                        } else {
                            name = null;
                        }
                    }
                    if (field.isAnnotationPresent(XmlElement.class)) {
                        if (name == null) {
                            name = field.getAnnotation(XmlElement.class).name();
                            if (!name.equals("##default")) {
                                name = context.toIdentifier(name);
                            } else {
                                name = null;
                            }
                        }
                    }
                    if (name == null) {
                        name = context.toIdentifier(field.getName());
                    }

                    if (!values.containsKey(name)) {
                        boolean accessible = field.isAccessible();
                        field.setAccessible(true);
                        try {
                            Object value = data == null ? null : field.get(data);
                            values.put(name, value);
                            types.put(name, value == null ? field.getType() : value.getClass());
                        } catch (Exception ex) {
                            throw new JSONException(ex);
                        }
                        field.setAccessible(accessible);
                    }
                }
            }
            c = c.getSuperclass();
        }

        for (String name : values.keySet()) {
            Object value = values.get(name);
            Class subType = types.get(name);
            result.put(name, buildSub(subType, value, typeTree));
        }

        depth--;
        return result;
    }

    private JSONObject buildRootObject(Class type, Object data, boolean typeTree) throws JSONException {
        depth++;

        JSONObject result = new JSONObject();
        String rootName = ((XmlRootElement) type.getAnnotation(XmlRootElement.class)).name();
        result.put(context.toIdentifier(rootName), buildObject(type, data, typeTree));

        depth--;
        return result;
    }

    private JSONObjectBase buildPrimitive(Object o) {
        if (o == null) {
            return new JSONNull();
        } else if (o instanceof Number) {
            return new JSONNumber((Number) o);
        } else if (o instanceof Date) {
            return new JSONNumber(((Date) o).getTime());
        } else if (o instanceof Boolean) {
            return new JSONBoolean((Boolean) o);
        }
        return new JSONString(o.toString());
    }

}
